5章 単純な業務ロジックを実装する
業務ロジックがかなり単純な場合に適した実装方法として、トランザクションスクリプトとアクティブレコードがある
トランザクションスクリプト
トランザクションスクリプトは、業務ロジックをより高度に実装する際の基礎
トランザクションスクリプトはデータを操作する手続きを単純に順次処理式のスクリプトとして、業務ロジックを記述する
この手続き処理が満たすべき唯一の要件は、トランザクション管理
処理は成功または、失敗で終わる必要がある
トランザクションスクリプトは見た目は単純だが、使い方を間違えやすい実装方法でもある
複数処理をひとつのトランザクションとして管理する観点が漏れるケース
複数データベースにデータを書き込む分散トランザクションの仕組みを導入しているケース
Pub/Subパターンを使ったコンポーネント間のやり取りを活用してデータベース更新を行っている場合、その通信基盤に問題があるとトランザクション管理が複雑になる
code:c++
public class LogVisit
{
public void Execute(Guid userId, DataTime viitedOn)
{
_db.Execute("UPDATE Users SET last_visite=@p1 WHERE user_id=@p2",
visitedOn, userId):
// ここの処理でエラーが発生した場合、上記の処理によってDBが更新されるが下記の処理は実行されないため
// データの一貫性が保てない
_messageBus.Publish("VISITS_TOPIC", new { UserId = user_id, VisitDate = visitedOn }):
}
}
暗黙的な分散トランザクションの構成になっているケース
https://scrapbox.io/files/6900c322a1a34f7a3572e057.png
code:c++
public class LogVisit
{
public void Execute(Guid userId)
{
_db.Execute("UPDATE Users SET visits=visits+1 WHERE user_id=@p1", userId);
}
}
上記の構成でLogVisitのメソッドが叩かれる場合、3.結果を得る前にclientとの通信が切れたら、DBへの書き込みはできているがclient側は処理が失敗したとして、再実行する可能性がある。その場合、結果1回の実行にも書かわずDBへの書き込みは2回行われてしまう。
この問題は解決するには、対象業務の特性と業務側の要求に依存する
client側が、訪問回数を実行時の引数として渡すようにする
何回実行しても、引数の訪問回数が同じであればDBの値は変わらず、冪等性を担保できる
code:c++
public void Execute(Guid userId, long visits)
{
_db.Execute("UPDATE Users SET visits=@1 WHERE user_id=@p2", visits, userId);
}
楽観的な排他制御を使って解決する方法
メソッドを呼び出す前に、現在の訪問数を取得し、メソッドの引数に渡す。渡された訪問数とDBの値が同じ時のみ更新する
code:c++
public void Execute(Guid userId, long expectedVisits)
{
_db.Execute(@"UPDATE Users SET visits=visits+1
WHERE user_id=@p1 and visits = @p2",
userId, expectedVisits);
}
トランザクションスクリプトをいつ使うか
業務ロジックが手続き的なデータ操作だけという単純は問題領域のとき
一般的な業務領域
中核の業務領域のような業務ロジックが複雑なケースに利用するとトランザクションスクリプトで同じロジックを記述する事になり、ロジックの一貫性が保てなくなる
アクティブレコード
アクティブレコードはトランザクションスクリプトと同様に、単純な業務ロジックの実装方法
リレーショナルなデータ操作をオブジェクト(ORM)を通して操作することができる
オブジェクトとして操作するので、永続化だけでなくデータ検証のような業務ロジックを含むことも可能
アクティブレコードをいつ使うか
CRUD操作と入力値の妥当性検査のような、簡単な業務ロジックに限定される以下のような場合
補完的な業務領域
一般的な業務領域用の外部サービスとの連携
区切られた文脈どうしを連携するときのモデル変換